A quick tutorial on how to make an interactive constituency result map using the Scottish Parliament Constituency Results from 2016. I will update this page with the 2021 results when they are released.
knitr::include_graphics("preview.jpg")

This will be a quick introduction to making interactive election result maps in R. I’ll be using the mapview package to do most of the heavy lifting. Click on each show code section to show the relevant annotated code on how I did each step. Copy anything you like - you can also find the full source code on my github page (link at the top of this page).
I downloaded the 2016 election results from The Electoral Commission. The constituency shapefile can be found on the ONS Website.
#Libraries----
library(sf)
library(readxl)
library(dplyr)
library(tidyr)
library(mapview)
library(leaflet)
library(leafpop)
library(ggplot2)
library(knitr)
library(scales)
#Read in the files=----
constituency_shp<-st_read("Scottish_Parliamentary_Constituencies_(May_2016)_Boundaries.shp")
results_df <- read_excel("Electoral-Data-Results-May-2016-Scottish-Parliament-elections.xls",
sheet = 'Constituencies - Results',
range = 'A2:AA75')
On our map we’re planning to show the colour of each party that won the constituency, and then when clicking on the map we want to show the breakdown in voting share on a bar chart.
To make that happen I’m splitting the winner and the vote share for each constituency into seperate dataframes.
#Remove some of the turnout information, not needed
results_df <- as.data.frame(results_df) %>%
select(-4:-17)
#Create a dataframe for the constituency winner----
winner_df <- results_df %>%
select(`ONS Code`,Constituency,Region,Win)
#Create a dataframe for the vote share by constituency
voteshare_df <- results_df %>%
select(-Win,-Second)
voteshare_df <- pivot_longer(voteshare_df,cols = c(4:11),names_to = "party",values_to = "share")
#Take first three letters of each party only
voteshare_df$party <- substr(voteshare_df$party,1,3)
This is what the “winner” dataset looks like:
| ONS Code | Constituency | Region | Win |
|---|---|---|---|
| S16000074 | Aberdeen Central | North East Scotland | SNP |
| S16000075 | Aberdeen Donside | North East Scotland | SNP |
| S16000076 | Aberdeen S. and N. Kincardine | North East Scotland | SNP |
| S16000077 | Aberdeenshire East | North East Scotland | SNP |
| S16000078 | Aberdeenshire West | North East Scotland | Con |
| S16000079 | Airdrie & Shotts | Central | SNP |
And this is the what the dataset of the vote share looks like:
| ONS Code | Constituency | Region | party | share |
|---|---|---|---|---|
| S16000074 | Aberdeen Central | North East Scotland | CON | 22.55 |
| S16000074 | Aberdeen Central | North East Scotland | LAB | 27.33 |
| S16000074 | Aberdeen Central | North East Scotland | LIB | 6.50 |
| S16000074 | Aberdeen Central | North East Scotland | SNP | 43.62 |
| S16000074 | Aberdeen Central | North East Scotland | IND | 0.00 |
| S16000074 | Aberdeen Central | North East Scotland | TUS | 0.00 |
I’m also creating a dataframe with colour codes associated with each party (these can be found on a very useful wikipedia index). When I join the colour codes to our shapefile later on, it means I don’t have to manually set any colours!
#Create a colour code df based on the unique parties
party_colours <- as.data.frame(unique(voteshare_df$party))
colnames(party_colours)[1] <- "party"
party_colours$colour_code <- c(
#con
"#0087DC",
#lab
"#E4003B",
#lib
"#FAA61A",
#snp
"#FDF38E",
#independent - just went with a gray colour here
"#696969",
#tusc,
"#EC008C",
#grn
"#00B140",
#other, another generic gray!
"#808080")
kable(head(party_colours))
| party | colour_code |
|---|---|
| CON | #0087DC |
| LAB | #E4003B |
| LIB | #FAA61A |
| SNP | #FDF38E |
| IND | #696969 |
| TUS | #EC008C |
In the next section I’m creating a list of ggplot objects. Basically, I’m making a bar chart for each constituency, which can then be linked on our map. Important for this method to work, is that the order in which the list is created matches up with the order of constituencies in our shapefile.
Below is an example of just one of the bar charts, the result from Edinburgh Southern.
#this might not be needed for the 2021 result, but for some reason
#there is an ONS code discrepancy between the results and our constituency shapefile.
#So here I manually change some codes
constituency_shp$spc16cd[which(constituency_shp$spc16cd == "S16000147")] <- voteshare_df$`ONS Code`[which(voteshare_df$Constituency=="Glasgow Provan")][1]
constituency_shp$spc16cd[which(constituency_shp$spc16cd == "S16000148")] <- voteshare_df$`ONS Code`[which(voteshare_df$Constituency=="Strathkelvin & Bearsden")][1]
#reordering our order of codes to match the shapefile
voteshare_df <- voteshare_df[order(voteshare_df$`ONS Code`),]
#reset rownumbers so they line up
rownames(voteshare_df) <- 1:nrow(voteshare_df)
#hopefully I won't have to repeat that step for the 2021 data!
#start of our list creation
bar_plot_list <- lapply(unique(voteshare_df$`ONS Code`), function(i) {
#filter and group info per ONS code
regio_df <- voteshare_df %>%
filter(`ONS Code` == i) %>%
dplyr::ungroup() %>%
select(party,share,Constituency) %>%
filter(share >0.1) %>%
as.data.frame()
#order by the vote share
regio_df <- regio_df[order(regio_df$share,decreasing = T),]
regio_df$share <- round(regio_df$share,1)
#rejoin with the colourscheme
regio_df <- left_join(regio_df,party_colours)
#now making the bar chart!
ggplot(regio_df) +
geom_bar(aes(x = party, y = share, fill = party),
stat = "identity",
colour = "black", width =1) +
#adding the vote share as text
geom_text(aes(x = party, y = share, label = paste0(share,"%")), vjust = -0.5, size = 3.5)+
#coord_polar("y", start = 0) +
labs(x = NULL, y = NULL, fill = NULL,
title = paste(unique(regio_df$Constituency))) +
theme_classic()+
#making sure it's ordered from biggest to smallest party
scale_x_discrete(limits = regio_df$party)+
#making sure the colours are automatically matched to the corresponding party
scale_fill_manual(values = regio_df$colour_code,
limits = regio_df$party)+
#setting the theme
theme(axis.line = element_blank(),
axis.text = element_blank(),
axis.ticks = element_blank(),
plot.title = element_text(hjust = 0.5, color = "black"),
legend.position = "bottom")
})
#creating the edinburgh south example using the same method as above
edi_south <- voteshare_df %>%
filter(`ONS Code` == "S16000108") %>%
dplyr::ungroup() %>%
select(party,share,Constituency) %>%
filter(share >0.1) %>%
as.data.frame()
edi_south <- left_join(edi_south,party_colours)
edi_south$share <- round(edi_south$share,1)
#reorder so it displays from biggest to smallest
edi_south <- edi_south[order(edi_south$share,decreasing = T),]
#plotting Edinburgh South to be displayed on the webpage as an example
ggplot(edi_south) +
geom_bar(aes(x = party, y = share, fill = party),
stat = "identity",
colour = "black", width =1) +
geom_text(aes(x = party, y = share, label = paste0(share,"%")), vjust = -0.5, size = 3.5)+
#coord_polar("y", start = 0) +
labs(x = NULL, y = NULL, fill = NULL,
title = paste("Constituency result",unique(edi_south$Constituency))) +
theme_classic()+
scale_x_discrete(limits = edi_south$party)+
scale_fill_manual(values = edi_south$colour_code,
limits = edi_south$party)+
theme(axis.line = element_blank(),
axis.text = element_blank(),
axis.ticks = element_blank(),
plot.title = element_text(hjust = 0.5, color = "black"),
legend.position = "bottom")

Now it’s time for our map! With our previous work and the mapview package, this is a surprisingly concise bit of code. As mentioned previously, all we have to do is link our winner dataset with our spatial consituencies by ONS code. Then we make sure the order is the same as our list of bar charts, and then put everything into the mapview function.
Clicking on each constituency will show a popup of the bar charts we made above.
#joining the constiuency shapefile with the winner dataframe
constituency_shp_mapping <- left_join(constituency_shp,winner_df, by = c("spc16cd"="ONS Code"))
#the shapefile and results excel sheet had slightly different naming conventions
#for the Liberal Democrats so we have to manually change these a little.
#make it all caps
constituency_shp_mapping$Win <- toupper(constituency_shp_mapping$Win)
#Swap LD to LIB
constituency_shp_mapping$Win <- gsub("LD","LIB",constituency_shp_mapping$Win)
#Now join with the colour codes
constituency_shp_mapping <- left_join(constituency_shp_mapping,party_colours,by = c("Win"="party"))
#reorder
constituency_shp_mapping <- constituency_shp_mapping[order(constituency_shp_mapping$spc16cd),]
#reset rownumbers so they line up
rownames(constituency_shp_mapping) <- 1:nrow(constituency_shp_mapping)
#calling our mapview function on the constituency_shp_mapping object
mapview(constituency_shp_mapping,
#specifying which column's information we want to show
zcol = "Win",
#and specify the colours, we can just select the relevant column
col.regions = constituency_shp_mapping$colour_code,
#and call on our barchart list to pop up
popup = popupGraph(bar_plot_list, width = 300,height =300),
legend = TRUE,
layer.name = "Party"
)
And there we go! A very quick way of creating an interactive election map for the Scottish Parliament’s constituencies in R.
Of course, don’t forget that there are also regional seats in the Scottish election under the AMS system - so this isn’t the full picture.